package org.ddevil.data.set;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.ddevil.data.Attribute;
import org.ddevil.data.BasicData;
import org.ddevil.data.Data;
import org.ddevil.data.util.DataSetIterator;
import org.ddevil.data.util.DataSetUtils;
import org.ddevil.data.util.DataUtils;


/**
 * A basic implementation Data Set implementation that works with double arrays of data.
 * Designed to work with the current framework where all results from the server are passed
 * back as double arrays.
 *
 * @author Eric Lundin
 * @author Rich
 */
public class BasicDataSet implements DataSet<Data> {

	private static final long serialVersionUID = 1395019925757729643L;

	private static final String INVALID_DBID_EXCEPTION =
            "Invalid Database Identifier for this Data Set: ";

	private static final String INVALID_COLUMN_SIZE_EXCEPTION =
            "The column count of the data being added does " +
            "not match the length of the dbIdentifiers array for this data set.";

	/**
	 * The DB Id's for each piece of data in this data set.
	 */
	final private String[] dbIdentifiers;

	/**
	 * Maps each DB Id with it's index in our data backing array.
	 */
	private Hashtable<String, Integer> dbIdToIndexMap;


	/**
	 * All the data in this data set.
	 */
	protected ArrayList<Object[]> recordSet;


    public BasicDataSet(List<String> dbIds){
        this(dbIds.toArray(new String[dbIds.size()]));
    }

	public BasicDataSet(String[] dbIDs) {
		this(new Object[0][dbIDs.length], dbIDs);
	}

    /**
     * Create a DataSet that is set up to hold the type of record
     * passed in.  Add the parameter as the only record.
     *
     * @param obj
     */
    public BasicDataSet(Data obj){
        this(extractData(obj), extractIDs(obj));
    }

    private static final String[]   extractIDs (Data in){
        String[] ret = new String[in.size()];
        int i = 0;
        for(Attribute di : in){
            ret[i++] = di.getId();
        }
        return ret;
    }
    private static final Object[][] extractData(Data in){
        Object[][] ret = new Object[1][in.size()];
        int i = 0;
        for(Attribute di : in){
            ret[0][i++] = di.getValue();
        }
        return ret;
    }

	/**
	 *
	 */
    public BasicDataSet(Object[][] data, String[] dbIdentifiers) {
    	this(data, dbIdentifiers, 0);
    }

    /**
     *
     * @param data -
	 *            The double array of data that backs this Data Set.
	 * @param dbIdentifiers -
	 *            The DB Identifiers for each column of data in this Data Set.
	 *            There is a one to one relationship between each element in
	 *            this array and each column in the data[][] array passed in as
	 *            the data backing. element [0] in this array ids column [0] and
	 *            so on.
     * @param dataOffset offset to use when reading the input data array
     */
	public BasicDataSet(Object[][] data, String[] dbIdentifiers, int dataOffset) {

	    /* set db ids and init our index lookup table */
        this.dbIdentifiers = dbIdentifiers;
        buildIdToIndexMap();

        /* store the actual data */
		recordSet = new ArrayList<Object[]>(data.length);
		addData(dataOffset, data);
	}


    /**
     * This constructor will build a BasicDataSet from a {@link ResultSet}.
     * The meta data will be used to get the db identifiers and each row
     * from the result set will be added to this data set.
     * This constructor does not handle any SQLExceptions that may occurr during
     * this process.
     *
     *
     *
     * TODO - evaluate this constructor
     *
     *
     * @param resultSet
     *          The ResultSet to build this Data Set from.
     *
     * @throws SQLException
     */
    public BasicDataSet(ResultSet resultSet) throws SQLException{

        /* first get the column ids from the meta data */

        ResultSetMetaData meta = resultSet.getMetaData();
        int columnCount = meta.getColumnCount();

        String[] columnIds = new String[columnCount];

        for(int i = 0; i < columnIds.length; i++){
            columnIds[i] = meta.getColumnName(i+1);
        }

        /* set db ids and init our index lookup table */
        this.dbIdentifiers = columnIds;
        buildIdToIndexMap();


        /* Add the data */



        recordSet = new ArrayList<Object[]>();
        while(resultSet.next()){

            Object[] row = new Object[columnCount];
            for(int i = 0; i < row.length; i++){
                row[i]  = resultSet.getObject(i+1);
            }
            addData(row);
        }
    }

    /*
     * When we set our db identifiers we also create this map for
     * faster column access using db ids.
     */
    private void buildIdToIndexMap(){
        /* set db ids and init our index lookup table */
        dbIdToIndexMap = new Hashtable<String, Integer>(dbIdentifiers.length);

        for (int i = 0; i < dbIdentifiers.length; i++) {
            dbIdToIndexMap.put(dbIdentifiers[i], i);
        }
    }

    /**
     * {@inheritDoc}
     */
    public List<String> getIdentifiers() {
        ArrayList<String> rv = new ArrayList<String>();
        for(String id: dbIdentifiers)
            rv.add(id);
        return rv;
    }

	/**
	 * {@inheritDoc}
	 */
	public Data exportRecord(int rowNum) {
        Object[] row = recordSet.get(rowNum);
        BasicData rv = new BasicData(dbIdentifiers, row);
		return  rv;
	}

    /**
     * {@inheritDoc}
     */
    public DataSet exportRecords(int... indices) {
        Object[][] data = new Object[indices.length][dbIdentifiers.length];

        for(int i = 0; i < indices.length; i++){
            data[i] = recordSet.get(indices[i]).clone();
        }
        BasicDataSet ret = new BasicDataSet(data, dbIdentifiers);
        return ret;
    }

	/**
	 * {@inheritDoc}
	 */
	public synchronized void importRecord(Data inData) {
		// attempt to import this data into our record set
		Object[] newRecord = new Object[dbIdentifiers.length];
		for (int i = 0; i < dbIdentifiers.length; i++) {
			newRecord[i] = inData.getValue(dbIdentifiers[i]);
		}

		addData(newRecord);
	}

    /**
     * {@inheritDoc}
     */
	public synchronized void importRecords(DataSet inData) {
	    Object[][] newData = DataSetUtils.getColumns(inData, dbIdentifiers);
        addData(newData);
	}

    /*
     * Add row(s) to the end of our recordSet
     */
	private synchronized void addData(Object[]... data) {
		addData(0, data);
	}
    private synchronized void addData(int offset, Object[]... data) {
        if(data.length > offset){
            //validate
        	// XXX commenting 8/26/08 - this is to restrictive
//            if (data[offset].length != dbIdentifiers.length) {
//                throw new IllegalArgumentException(INVALID_COLUMN_SIZE_EXCEPTION);
//            }

    		for (int i = offset; i < data.length; i++) {
    			recordSet.add(data[i]);
    		}
        }
	}


	/**
	 * {@inheritDoc}
	 */
	public synchronized void setDataAt(int rowIndex, String dbIdentifier, Object data) {
        int colIndex = getIndexOfDBId(dbIdentifier);
        if(colIndex > -1){
            recordSet.get(rowIndex)[getIndexOfDBId(dbIdentifier)] = data;
        }else{
            throw new IllegalArgumentException(INVALID_DBID_EXCEPTION+dbIdentifier);
        }
	}

    /**
     *  {@inheritDoc}
     */
    public synchronized Object getDataAt(int rowIndex, String dbIdentifier){
        Object[] row = recordSet.get(rowIndex);
        int colIndex = getIndexOfDBId(dbIdentifier);
        if(colIndex > -1)
            return row[colIndex];
        return null;
    }

  	/**
	 * {@inheritDoc}
	 */
	public synchronized void removeRecord(int index) {
		recordSet.remove(index);
	}

	/**
	 * {@inheritDoc}
	 */
	public synchronized void clear() {
		recordSet.clear();
	}

	/**
	 * {@inheritDoc}
	 */
	public int size() {
		return recordSet.size();
	}

	/**
	 * Set the data in this data set to the given data.
	 *
	 * @param records
	 */
	public synchronized void setData(Object[][] data) {
		recordSet.clear();
		addData(data);
	}

	/**
	 * Convenience method that gets the index of the given DB Id from our map.
	 *
	 * @param dbIdentifier
	 *            The DB Id we want the index of.
	 * @return The index of the given DB Id, or -1 if it doesnt exists.
	 */
	private int getIndexOfDBId(String dbIdentifier) {
	    Integer index = dbIdToIndexMap.get(dbIdentifier);
	    if (index != null) {
	        return index;
	    }
	    return -1;
	}

	/**
	 * Deep clone object to prevent reference being held by components using
	 * this dataset.
	 */
	@Override
    public DataSet clone() {
		DataSet clonedSet;
		Object[][] data;
		data = new Object[recordSet.size()][];
		for (int i = 0; i < recordSet.size(); i++) {
			data[i] = recordSet.get(i);
		}
		clonedSet = new BasicDataSet(data, dbIdentifiers);
		return clonedSet;
	}

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Data Set:\n");
        sb.append("---------\n");
        sb.append("Records: " + size() + "\n\n");

        sb.append("Raw data column to DB Id mapping:\n");

        String[][] mapping = new String[dbIdentifiers.length][2];
        for(int i = 0; i < dbIdentifiers.length; i++){
            mapping[i][0] = String.valueOf(i);
            mapping[i][1] = dbIdentifiers[i];
        }
        sb.append(DataUtils.formatDoubleArrayForPrint(mapping));


        return sb.toString();
    }

	public Iterator<Data> iterator() {
		return new DataSetIterator<Data>(this);
	}

}
